package script.grammar.symbol;

import script.grammar.LocScriptAST;
import script.grammar.expression.Expression;
import event.DebugLogger;
import event.ErrorLogger;

public class SymbolTable {
	public static final Symbol UNRESOLVED_SYMBOL = new Symbol("Unresolved", NativeType.VOID);
	
	private Scope globalScope;
	private ClassSymbol classScope;
	private Scope localScope;
	private DebugLogger d;
	private ErrorLogger e;
	
	private Expression expr;
	
	public SymbolTable(Scope globalScope, ClassSymbol classScope, DebugLogger d, ErrorLogger e) {
		this.globalScope = globalScope;
		this.classScope = classScope;
		this.localScope = new BaseScope("Local Scope", classScope);
		this.expr = new Expression(this);
		this.d = d;
		this.e = e;
		
		populateTypeSymbols();
	}
	
	/**
	 * Returns the global scope of this symbol table. The global scope contains all 
	 * symbols and definitions used by the user script.
	 * @return the global scope of this symbol table.
	 */
	public Scope getGlobalScope() {
		return globalScope;
	}
	
	/**
	 * Returns the class scope of this symbol table. The class scope determines what 
	 * member functions the user can call.
	 * @return the class scope of this symbol table.
	 */
	public ClassSymbol getClassScope() {
		return classScope;
	}
	
	/**
	 * Returns the local scope of this symbol table. The local scope is the base scope 
	 * for all user-defined code.
	 * @return the local scope of this symbol table.
	 */
	public Scope getLocalScope() {
		return localScope;
	}
	
	/**
	 * Attempts to add a variable definition to this symbol table. If the definition is 
	 * accepted, the symbol and its type are added to the symbol table. If for any reason 
	 * the definition is rejected, the token will be marked as an unresolved symbol.
	 * <p>If the name already exists in the same scope, or if the type symbol can't be 
	 * resolved, the definition will be rejected and an error logged. If the initialization 
	 * expression evaluates to a type incompatible with the new variable, an error will be 
	 * logged but the definition will still be accepted as long as neither of the two 
	 * conditions listed above are met.
	 * @param name the AST node representing the name token.
	 * @param typeName the AST node representing the type token.
	 * @param init the AST node pointing to the initialization expression, if any.
	 */
	public void defineVariable(LocScriptAST name, LocScriptAST typeName, LocScriptAST init) {
		// resolve type symbol
		Symbol typeSymbol = resolve(name.scope, typeName);
		if (!(typeSymbol instanceof TypeSymbol || typeSymbol instanceof ClassSymbol)) {
			e.logErrorMsg("Line " + typeName.getLine() + ": '" + typeName.getText() + "' is not a valid type.");
		}
		// attempt to add defn
		VariableSymbol symbol = new VariableSymbol(name.getText(), typeSymbol.getType());
		if (name.scope.define(symbol)) {
			name.symbol = symbol;
		} else {
			name.symbol = UNRESOLVED_SYMBOL;
			e.logErrorMsg("Line " + name.getLine() + ": Duplicate definition '" + name.getText() + "'.");
		}
		
		// check type of initialization expr (if there is any)
		if (init != null) {
			if (!typeSymbol.getType().equals(init.evalType)) {
				if (!(typeSymbol.getType().equals(NativeType.DOUBLE) && init.evalType.equals(NativeType.INT))) {
					e.logErrorMsg("Line " + typeName.getLine() + ": variable of type " + typeSymbol.getType().getName()
							+ " cannot be initialized with a value of type " + init.evalType.getName() + ".");
				}
			}
		}
	}
	
	/**
	 * Attempts to resolve a token within a scope.
	 * @param scope the scope in which to search for the symbol.
	 * @param name the AST node representing the token to be resolved.
	 * @return the Symbol to which the token resolves.
	 */
	public Symbol resolve(Scope scope, LocScriptAST name) {
		Symbol s = scope.resolve(name.getText());
		if (s == null) {
			s = UNRESOLVED_SYMBOL;
			e.logErrorMsg("Line " + name.getLine() + ": symbol '" + name.getText() + "' could not be resolved.");
		}
		name.symbol = s;
		return s;
	}
	
	/**
	 * Attempts to resolve a variable name token.
	 * @param name the AST node representing the token to be resolved.
	 * @return the Symbol to which the token resolves.
	 */
	public Symbol resolveVariable(LocScriptAST name) {
		return resolve(name.scope, name);
	}
	
	/**
	 * Attempts to resolve a member name token.
	 * @param parent the AST node representing the class name token representing 
	 * the parent of the member. The node must point to a token that resolves to 
	 * a ClassSymbol.
	 * @param name the AST node representing the token to be resolved.
	 * @return the Symbol to which the token resolves.
	 */
	public Symbol resolveMember(LocScriptAST parent, LocScriptAST name) {
		if (!(parent.evalType instanceof ClassSymbol)) {
			e.logErrorMsg("Line " + name.getLine() + ": member access operator '.' cannot be used on type '" 
					+ parent.evalType + "'.");
			return UNRESOLVED_SYMBOL;
		}
		return resolve((ClassSymbol)parent.evalType, name);
	}
	
	/**
	 * Attempts to resolve a member name token whose parent is assumed to be 
	 * this symbol table's class scope.
	 * @param name the AST node representing the token to be resolved.
	 * @return the Symbol to which the token resolves.
	 */
	public Symbol resolveMember(LocScriptAST name) {
		return resolve(getClassScope(), name);
	}
	
	/**
	 * Computes the type to which an operation evaluates.
	 * @param op the AST node representing the operator token.
	 * @param operands the AST nodes representing the operand tokens.
	 * @return the Type to which the operation evaluates.
	 */
	public Type computeType(LocScriptAST op, LocScriptAST... operands) {
		Type[] opTypes = new Type[operands.length];
		for (int i = 0; i < operands.length; ++i) {
			opTypes[i] = operands[i].evalType;
		}
		Type val = expr.computeType(op.getType(), opTypes);
		if (val.equals(NativeType.VOID)) {
			String s = "";
			for (int i = 0; i < operands.length; ++i) {
				s += operands[i].evalType.getName() + ", ";
			}
			e.logErrorMsg("Line " + op.getLine() + ": operator '" + expr.getOperatorFromIndex(op.getType()).getName()
					+ "' is incompatible with the types: " + s.substring(0, s.length() - 2));
		}
		
		return val;
	}
	
	public Type handleArrayAccess(LocScriptAST ar, LocScriptAST index) {
		if (!(ar.evalType instanceof ArrayType)) {
			e.logErrorMsg("Line " + ar.getLine() + ": [] operator can only be used on array types.");
			return NativeType.VOID;
		} else if (!(index.evalType.equals(NativeType.INT))) {
			e.logErrorMsg("Line " + ar.getLine() + ": [] operator can only accept arguments of type int.");
			return NativeType.VOID;
		}
		return ((ArrayType)ar.evalType).getElementType();
	}
	
	public void handleReturn(LocScriptAST expr) {
		if (!NativeType.BOOLEAN.equals(expr.evalType)) {
			e.logErrorMsg("Line " + expr.getLine() + ": script must return a value of type boolean.");
		}
	}
	
	public void handleForEach(LocScriptAST def, LocScriptAST expr) {
		Type varType = ((LocScriptAST)def.getChild(0)).symbol.getType();
		Type listType = expr.evalType;
		
		if (listType instanceof ArrayType) {
			if (!varType.equals(((ArrayType)listType).getElementType())) {
				e.logErrorMsg("Line " + def.getLine() + ": foreach variable definition type '" + varType.getName()
						+ "' does not match list element type '" + ((ArrayType)listType).getElementType().getName() + "'.");
			}
		} else {
			e.logErrorMsg("Line " + expr.getLine() + ": foreach loop cannot iterate over type '" + listType.getName() + "'.");
		}
	}
	
	private void populateTypeSymbols() {
		globalScope.define(NativeType.INT.getTypeSymbol());
		globalScope.define(NativeType.DOUBLE.getTypeSymbol());
		globalScope.define(NativeType.BOOLEAN.getTypeSymbol());
	}
}
